﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;

namespace JsValidator
{
    internal class Processor
    {
        private readonly IReporter _reporter;
        private readonly string _configPath;
        private readonly string _basePath;
        private Config _config;

        internal string JavaPath { get; set; }

        public Processor(IReporter reporter, string configPath)
            : this(reporter, configPath, Path.GetDirectoryName(configPath))
        {
        }

        internal Processor(IReporter reporter, string configPath, string basePath)
        {
            _reporter = reporter;
            _configPath = configPath;
            _basePath = basePath;
            JavaPath = "java.exe";
        }

        public void Process()
        {
            try
            {
                _config = Config.Deserialize(File.ReadAllText(_configPath));
            }
            catch (Exception ex)
            {
                _reporter.ReportError("Error parsing config file " + ex.Message);
                return;
            }

            try
            {
                ValidateJsAssets();
            }
            catch (Exception ex)
            {
                _reporter.ReportError(ex.Message);
            }

        }

        private void ValidateJsAssets()
        {
            var programDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
            var closureJarPath = Path.Combine(programDir, "lib\\closure-compiler.jar");

            if (!File.Exists(closureJarPath))
            {
                _reporter.ReportError("closure-compiler.jar not found at " + closureJarPath);
                return;
            }

            var validatorCommandArgs = new StringBuilder(1000);

            var jsFiles = GatherJsAssets(_config);
            if (jsFiles.Count == 0)
            {
                _reporter.ReportError("no js files specified to process (or no files were found on disk). Check paths!");
                return;
            }
            foreach (var jsFile in jsFiles)
                validatorCommandArgs.AppendFormat("--js \"{0}\" ", jsFile);

            foreach (var @extern in _config.Externs)
            {
                var externsDir = programDir;
                var filePath = "";
                if (@extern.StartsWith("//"))
                {
                    var downloadableExternsDir = Path.Combine(externsDir, "google");
                    var fileName = @extern.Trim('/');

                    var externPath = Path.Combine(downloadableExternsDir, fileName);
                    if (!File.Exists(externPath))
                    {
                        Directory.CreateDirectory(downloadableExternsDir);
                        DownloadExtern(fileName, externPath);
                    }

                    filePath = externPath;
                }
                else
                {
                    filePath = GetPathTo(@extern);
                }

                validatorCommandArgs.AppendFormat("--externs \"{0}\" ", filePath);
            }

            validatorCommandArgs.AppendFormat("--warning_level {0} ", EscapeArg(_config.WarningLevel));
            validatorCommandArgs.AppendFormat("--compilation_level {0} ", EscapeArg(_config.CompilationLevel));
            validatorCommandArgs.Append(_config.Ccargs);

            var validatorArgs = new ProcessStartInfo(JavaPath)
                                    {
                                        Arguments =
                                            String.Format("-jar \"{0}\" {1} --js_output_file \"{2}\" ", closureJarPath,
                                                          validatorCommandArgs, _config.OutputFile),
                                        UseShellExecute = false,
                                        RedirectStandardError = true
                                    };

            try
            {
                using (var validator = new Process())
                {
                    validator.StartInfo = validatorArgs;
                    validator.Start();

                    var errorMessages = validator.StandardError.ReadToEnd();
                    validator.WaitForExit();

                    var errorsFound = false;
                    WriteJsAssetsErrors(errorMessages, out errorsFound);
                    if (((validator.ExitCode != 0) && _config.IgnoreErrors.Count == 0) && !errorsFound)
                    {
                        _reporter.ReportError(errorMessages);
                        return;
                    }
                }
            }
            catch (Exception ex)
            {
                _reporter.Report("Java not installed or not added to system PATH", _config.TreatNoJavaAsWarning ? ReportEntryType.Warning : ReportEntryType.Error);
            }

        }

        private void DownloadExtern(string url, string saveTo)
        {
            var client = new WebClient();
            client.DownloadFile("http://closure-compiler.googlecode.com/svn/trunk/contrib/externs/" + url, saveTo);
        }

        private string EscapeArg(string arg)
        {
            return arg.Contains(' ') ? "\"" + arg + "\"" : arg;
        }
        private string GetPathTo(string relativeFilePath)
        {
            var path = Path.Combine(_basePath, relativeFilePath);
            path = Path.GetFullPath(path);

            return path;
        }

        private List<string> GatherJsAssets(Config config)
        {
            var jsFiles = new List<string>();

            foreach (String dir in config.Inputs)
            {
                var path = GetPathTo(dir);
                if (Directory.Exists(path))
                {
                    jsFiles.AddRange(Directory.GetFiles(path, "*.js", SearchOption.AllDirectories));
                }
                else
                {
                    if (File.Exists(path) && path.EndsWith(".js"))
                    {
                        jsFiles.Add(path);
                    }
                }
            }

            return jsFiles.Distinct().ToList();
        }

        private void WriteJsAssetsErrors(String errorMessages, out bool errorsFound)
        {
            errorsFound = false;
            var errorMatcher = new Regex(@"^(?<fullPath>.+):(?<lineNumber>\d+): (?<level>WARNING|ERROR) \- (?<message>.+)");

            var tempFileName = Path.GetTempPath() + "JsValidator.log";
            File.WriteAllText(tempFileName, errorMessages);

            errorMessages = PostProcessErrorMessages(errorMessages);
            errorMessages = PostProcessWarnings(errorMessages);

            File.WriteAllText(tempFileName + "_", errorMessages);

            String[] errors = errorMessages.Split('\n');

            for (int i = 0; i < errors.Length; i++)
            {
                Match errorMatch = errorMatcher.Match(errors[i]);

                if (!errorMatch.Success)
                    continue;

                ReportEntryType entryType;
                switch (errorMatch.Groups["level"].Value.ToLower())
                {
                    case "warning":
                        entryType = ReportEntryType.Warning;
                        break;
                    case "error":
                        entryType = ReportEntryType.Error;
                        break;

                    default:
                        continue;
                }

                if (entryType == ReportEntryType.Warning && _config.IgnoreAllWarnings)
                    continue;

                var errorStartColumn = errors[i + 2].LastIndexOfAny(new char[] { ' ', '\t' }) + 2;

                if (entryType == ReportEntryType.Error)
                    errorsFound = true;

                _reporter.Report(entryType,
                    errorMatch.Groups["message"].Value,
                    errorMatch.Groups["fullPath"].Value,
                    errorMatch.Groups["lineNumber"].Value,
                    errorStartColumn);
            }
        }

        private string PostProcessWarnings(string errorMessages)
        {
            foreach (var warning in _config.IgnoreWarnings)
            {
                errorMessages = RemoveWarning(errorMessages, warning);
            }

            return errorMessages;
        }


        private string PostProcessErrorMessages(string errorMessages)
        {
            foreach (var warning in _config.Warnings)
            {
                errorMessages = ReplaceWarningWithError(errorMessages, warning);
            }
            foreach (var errorRegex in _config.IgnoreErrors)
            {
                errorMessages = RemoveError(errorMessages, errorRegex);
            }

            return errorMessages;
        }

        private string ReplaceWarningWithError(string res, string warningRegex)
        {
            return Regex.Replace(res, String.Format("WARNING - (?<text>.*?{0}.*?)", warningRegex), "ERROR - ${text}", RegexOptions.IgnoreCase);
        }


        private string RemoveWarning(string res, string warningRegex)
        {
            return Regex.Replace(res, String.Format("WARNING - (?<text>.*?{0}.*?)", warningRegex), "", RegexOptions.IgnoreCase);
        }

        private string RemoveError(string res, string errorRegex)
        {
            return Regex.Replace(res, String.Format("ERROR - (?<text>.*?{0}.*?)", errorRegex), "", RegexOptions.IgnoreCase);
        }
    }
}